package Day_0219.Redis;

/**
 * @author zxc
 * @date 2023/02/19 23:24
 **/
public class Cache_DataBase_ReadWrite_Consistency {
    public static void main(String[] args) {
        /**
         * 如何保证缓存和数据库的双写一致性
         * ===》
         * 1.一般来说，如果允许缓存可以稍微地跟数据库偶尔有不一致的情况，
         * 也就是说，如果系统不是严格要求“缓存+数据库” 必须保持一致性的话，最好不要做这个方案，
         * ===》即：读请求和写请求串行化，串到一个内存队列里去。
         *
         * 2.读请求和写请求串行化，可以保证 不会出现数据不一致的情况，
         * 但是它也会导致系统的吞吐量大幅度降低，用比正常情况下多几倍的机器去支撑线上的一个请求。
         *
         * 3.为保证数据库和缓存的读写一致性
         * ===》
         * Cache Aside Pattern
         * 最经典的缓存+数据库读写的模式，就是 Cache Aside Pattern。
         *（1）- 读的时候，先读缓存，缓存没有的话，就读数据库，然后取出数据后，放入缓存，同时返回响应。
         *（2）- 更新的时候，先更新数据库，然后再删除缓存。
         *
         * 4.为什么是删除缓存，而不是更新缓存？
         * （1）原因很简单，很多时候，在复杂点的缓存场景，缓存不单单是数据库中直接取出来的值。
         *  比如，可能更新了某个表的一个字段，然后其对应的缓存，
         *  是需要查询另外两个表的数据并进行运算，才能计算出缓存最新的值的。
         *
         * （2）更新缓存的代价有时候是很高的。
         *  是不是说，每次修改数据库的时候，都一定要将其对应的缓存更新一份？
         *  也许有的场景是这样，但是对于比较复杂的缓存数据计算的场景，就不是这样了。
         *  如果你频繁修改一个缓存涉及的多个表，缓存也频繁更新。
         *  但是问题在于，这个缓存到底会不会被频繁访问到？
         *  ===》
         *  案例 ：
         *  举个例子，一个缓存涉及的表的字段，在 1 分钟内就修改了 20 次，或者是 100 次，那么缓存更新 20次、100 次；
         *  但是这个缓存在 1 分钟内只被读取了 1 次，有大量的冷数据。
         *  实际上，如果你只是删除缓存的话，那么在 1 分钟内，这个缓存不过就重新计算一次而已，开销大幅度降低。
         *  用到缓存才去算缓存。
         *
         * （3）其实删除缓存，而不是更新缓存，就是一个 lazy 计算的思想，
         *  不要每次都重新做复杂的计算，不管它会不会用到，而是让它到需要被使用的时候再重新计算。
         *  ===》
         *  类似于 惰性更新缓存
         *  （在缓存对应数据库中的字段被修改后，删除缓存，只有等到缓存被访问的时候，才会去数据库中获取数据，同时加载缓存）
         *
         *  案例 ：
         *  像 mybatis，hibernate，都有懒加载思想。
         *  查询一个部门，部门带了一个员工的 list，没有必要说每次查询部门，都里面的 1000 个员工的数据也同时查出来啊。
         *  80%的情况，查这个部门，就只是要访问这个部门的信息就可以了。
         *  先查部门，同时要访问里面的员工，那么这个时候只有在你要访问里面的员工的时候，才会去数据库里面查询1000个员工。
         *
         *
         * 4.缓存和数据库中数据不一致的解决方案
         * 1）先更新数据库中数据，再去删除缓存;
         * ===》
         * （1）即，若是更新完数据库后，删除缓存的操作失败的话，则缓存中所存储的数据就是旧数据，但是此时数据库中数据已经被更新，
         * 所以会出现短暂的数据不一致情况;
         * （2）在高并发的场景下，若是 更新数据库中数据操作还未完成（即，数据库中所存储的数据仍然是旧数据）时，
         *  则有请求来获取数据，就直接获取缓存中的旧数据（数据库中仍然是旧数据）;
         *  ===》未出现缓存和数据库中数据不一致的情况。
         *
         * 2）先删除缓存，再去更新数据库中的值
         * ===》
         * （1）即，若是先删除缓存的话，同时更新数据库操作失败的话，此时客户端请求该数据，会去数据库中读取，
         *  由于数据库中数据并未修改，所以缓存重新建立，仍然是旧数据;
         * （2）在高并发的场景下，若是删除缓存 && 在更新数据库（还未更新数据库）过程中，同时有一个请求来获取该数据，
         *  即，会直接去数据库中读取数据，由于数据库中还保存着旧数据，所以缓存中所存储还是旧数据，
         *  接着，数据库中数据更新完成，数据库中的数据为新数据 && 缓存中数据为旧数据，出现了缓存和数据库数据不一致的情况;
         *
         * 5.解决（在高并发情况，先删除缓存后更新数据库所带来的数据不一致问题）的方案 ：
         * （1）更新数据的时候，根据数据的唯一标识，将操作路由之后，发送到一个 jvm 内部队列中。
         *  读取数据的时候，如果发现数据不在缓存中，那么将重新读取数据+更新缓存的操作，
         *  根据唯一标识路由之后，也发送同一个jvm 内部队列中。
         *
         * （2）一个队列对应一个工作线程，每个工作线程串行拿到对应的操作，然后一条一条的执行。
         *  这样的话一个数据变更的操作，先删除缓存，然后再去更新数据库，但是还没完成更新。
         *  此时如果一个读请求过来，没有读到缓存，那么可以先将缓存更新的请求发送到队列中，此时会在队列中积压，
         *  然后，同步等待缓存更新完成。
         *
         * （3）优化点 ：一个队列中，其实多个更新缓存请求串在一起是没意义的，
         *  因此可以做过滤，如果发现队列中已经有一个更新缓存的请求了，
         *  那么就不用再放个更新请求操作进去了，直接等待前面的更新操作请求完成即可。
         *
         * （4）待那个队列对应的工作线程完成了上一个操作的数据库的修改之后，才会去执行下一个操作，
         *  也就是缓存更新的操作，此时会从数据库中读取最新的值，然后写入缓存中。
         *
         * （5）如果请求还在等待时间范围内，不断轮询发现可以取到值了，那么就直接返回；
         *  如果请求等待的时间超过一定时长，那么这一次直接从数据库中读取当前的旧值。
         *
         * ===》
         * （1）即，维护一个内部队列，来存储更新数据库，重构缓存的操作;
         * （2）即，若是在更新数据库操作执行过程中，有读取请求（即，需要去重建缓存），则会将更新缓存的操作压入内部队列中，
         * 等待至更新数据库操作完成后，在去更新缓存，保证缓存和数据库数据的一致性;
         * （3）若是有大量的更新数据库操作的话，会导致之前的读请求失效，失效的读请求会直接去数据库中读取数据，导致数据库压力增大;
         *
         *
         * 6.高并发的场景下，解决（先删除缓存，再更新数据库）所需要注意的问题：
         * 1）读请求长时阻塞
         * 由于读请求进行了非常轻度的异步化，所以一定要注意读超时的问题，每个读请求必须在超时时间范围内返回。
         * （1）该解决方案，最大的风险点在于说，可能数据更新很频繁，导致队列中积压了大量更新操作在里面，
         *  然后读请求会发生大量的超时，最后导致大量的请求直接走数据库。
         *  务必通过一些模拟真实的测试，看看更新数据的频率是怎样的。
         * （2）另外一点，因为一个队列中，可能会积压针对多个数据项的更新操作，
         *  因此需要根据自己的业务情况进行测试，可能需要部署多个服务，每个服务分摊一些数据的更新操作。
         *  ===》
         *  如果一个内存队列中可能积压的更新操作特别多，那么你就要加机器，让每个机器上部署的服务实例处理更少的数据，
         *  那么每个内存队列中积压的更新操作就会越少。
         *  其实根据之前的项目经验，一般来说，数据的写频率是很低的，因此实际上正常来说，在队列中积压的更新操作应该是很少的。
         *  像这种针对读高并发、读缓存架构的项目，一般来说写请求是非常少的，每秒的 QPS 能到几百就不错了。
         *
         * 2）读请求并发量过高
         * （1）这里还必须做好压力测试，确保恰巧碰上上述情况的时候，还有一个风险，
         *  就是突然间大量读请求会在几十 毫秒的延时 hang 在服务上，看服务能不能扛的住，
         *  需要多少机器才能扛住最大的极限情况的峰值。
         * （2）但是因为并不是所有的数据都在同一时间更新，缓存也不会同一时间失效，
         * 所以每次可能也就是少数数据的缓存失效了，然后那些数据对应的读请求过来，并发量应该也不会特别大。
         *
         * 3）多服务实例部署的请求路由
         * （1）可能这个服务部署了多个实例，
         *  那么必须保证说，执行数据更新操作，以及执行缓存更新操作的请求，都通过 Nginx 服务器路由到相同的服务实例上。
         * （2）比如说，对同一个商品的读写请求，全部路由到同一台机器上。
         *  可以自己去做服务间的按照某个请求参数的hash 路由，也可以用 Nginx 的 hash 路由功能等等。
         * ===》
         * 即，内部队列并不是分布式的，不能够被多台机器进行共享;
         * 所以，若是想要保证更新数据库操作 && 缓存更新操作是同步的话，需要使用同一个内部队列
         * （即，需要使得该操作有同一台机器进行处理;）
         *
         * 4）热点商品的路由问题，导致请求的倾斜
         * （1）万一某个商品的读写请求特别高，全部打到相同的机器的相同的队列里面去了，可能会造成某台机器的压力过大。
         *  就是说，因为只有在商品数据更新的时候，才会清空缓存，然后才会导致读写并发，
         * （2）所以其实要根据业务系统去看，如果更新频率不是太高的话，这个问题的影响并不是特别大，
         *  但是的确可能某些机器的负载会高一些。
         *
         */
    }
}
